package br.ufrj.dcc.xml.dynamicintervals.decoder;

/**
 * @author Alexandre Oliveira
 * This classes decodes the Internal Encoding as described in 
 * "A Comprehensive XQuery to SQL Translation using Dynamic 
 * Interval Encoding", page 4, Definition 3.1, back into XML. 
 */

import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.EmptyStackException;
import java.util.Iterator;
import java.util.Stack;

import br.ufrj.dcc.xml.dynamicintervals.vo.Item;

public abstract class Decoder {

	private Stack<Item> stack = new Stack<Item>();

	// onAttributeList means there's a starting element tag open
	// ( sth like "<tag" ), waiting for a possible list of elements.
	private boolean onAttributeList = false;
	
	private boolean keepFormat = true;
	private final int TABLENGTH = 2;

	public boolean isKeepFormat() {
		return keepFormat;
	}

	public void setKeepFormat(boolean keepFormat) {
		this.keepFormat = keepFormat;
	}

	/**
	 * Decodes one item. Don't assume it will output all the content of the
	 * item, as it may have to wait for other items (for instance, a element is
	 * only closed after all its content was output).
	 * 
	 * @param item
	 *            The Item that will be decoded
	 * @throws IOException
	 *             On output errors
	 */

	public void decodeNext(Item item) throws IOException {
		// Check if we should close an opening tag
		closeAttributeListIfApplicable(item);

		if (stack.size()>0) {
			Item topStack = stack.peek();
			if (item.getType()==Item.ELEMENT)
				topStack.setHasElementChildren(true);
			if (item.getType()==Item.DATA)
				topStack.setHasDataChildren(true);
			
		}
		// Check if items on stack should be closed
		verifyOpenElements(item);

		String data = item.getData();
		switch (item.getType()) {
		case Item.ELEMENT:
			onAttributeList = true;
			if (!keepFormat && stack.size()>0) {
				print("\n");
			}
			print(ident() + "<" + data);
			stack.push(item);
			item.setHasElementChildren(false);
			item.setHasDataChildren(false);
			break;
		case Item.ATTRIBUTE:
			print(' ' + data + '=');
			break;
		case Item.ATTRIBUTEVALUE:
			print('\"' + data + '\"');
			break;
		case Item.DATA: 
			if (keepFormat) 
				print(data);
			else if (data.trim().length()>0)
				print(data.trim());
			break;
		case Item.PROCESSINGINSTRUCTION:
			print(ident() + "<?" + data + "?>");
		}
	}

	private String ident() {
		if (keepFormat)
			return "";
		int l = stack.size()*TABLENGTH;
		String s = "";
		for(int i=0; i<l; ++i) {
			s+=' ';
		}
		return s;
	}

	/**
	 * Decodes a Collection of items. Only use this function when you have the
	 * whole document in the collection. If you need to process it in parts, use
	 * decodeNext on it item.
	 * 
	 * @param items
	 *            A *ordered* list of Items. It will call decodeOne on each item
	 *            of the list, followed by flush in the end.
	 * @throws IOException
	 *             On output errors
	 */
	public void decode(Collection<Item> items) throws IOException {

		Iterator<Item> it = items.iterator();
		while (it.hasNext()) {
			Item item = it.next();
			decodeNext(item);
		}
		// cleaning stack
		flush();
	}

	public void decodeRS(ResultSet rs) throws IOException, SQLException {
		while (rs.next()) {
			Item item = new Item();
			item.setData(rs.getString("data"));
			item.setL(rs.getInt("l"));
			item.setR(rs.getInt("r"));
			item.setType(rs.getInt("type"));
			decodeNext(item);
		}
		// cleaning stack
		flush();
	}

	/**
	 * It *must* be called after and only after the last element is sent to
	 * decodeNext. It will print any output that is left. Calling it before the
	 * last element is decoded will produce wrong results.
	 * 
	 * @throws IOException
	 *             On output errors
	 */

	public void flush() throws IOException {
		while (stack.size() > 0) {
			Item lastItem = stack.pop();
			if (!keepFormat && lastItem.isHasElementChildren())
				print('\n'+ident());
			print("</" + lastItem.getData() + '>');
		}
	}

	/**
	 * Checks whether we should close a still open preceding starting tag. It'll
	 * be closed when item is neither an attribute nor a attibute value
	 * 
	 * @param item
	 * @throws IOException
	 *             On output errors
	 */
	protected void closeAttributeListIfApplicable(Item item) throws IOException {
		if (onAttributeList
				&& (item.getType() != Item.ATTRIBUTE && item.getType() != Item.ATTRIBUTEVALUE)) {
			
			if (!keepFormat && item.getType() == Item.DATA && item.getData().trim().length() <=0)
				return;
			
			onAttributeList = false;
			
			if (!keepFormat && (item.getType() == Item.ELEMENT || item.getType() == Item.PROCESSINGINSTRUCTION))  {
				Item lastItem = stack.peek();
				if (lastItem.getR() < item.getL() && !lastItem.isHasDataChildren()) {
					print("/>");
					stack.pop();
					return;
				}
			}
			// fim da lista de atributos
			print(">");
		}
	}

	/**
	 * Checks if elements on the stack should have their closing tag added
	 * before processing item
	 * 
	 * @param item
	 *            The item that's going to be processed
	 * @throws IOException
	 *             On output errors
	 */
	protected void verifyOpenElements(Item item) throws IOException {
		try {
			for (Item lastItem = stack.peek(); lastItem.getR() < item.getL(); lastItem = stack
					.peek()) {
				stack.pop();
				if (!keepFormat && onAttributeList) {
					print("/>");
					onAttributeList=false;
				}
				else {
					if (!keepFormat && lastItem.isHasElementChildren() )
						print("\n"+ident());
					print("</" + lastItem.getData() + ">");
				}
			}
		} catch (EmptyStackException e) {
			// live with that (AKA do nothing)
		}
	}

	protected abstract void print(String s) throws IOException;
}
